package net.barbierdereuille.android.AndroBib;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import net.barbierdereuille.android.AndroBib.AndroBib.AuthorColumn;
import net.barbierdereuille.android.AndroBib.AndroBib.BibTexColumn;
import net.barbierdereuille.android.AndroBib.AndroBib.FieldColumn;
import net.barbierdereuille.android.AndroBib.AndroBib.GlobalColumn;
import net.barbierdereuille.android.AndroBib.AndroBib.PreambleColumn;
import net.barbierdereuille.android.AndroBib.AndroBib.StringColumn;
import net.barbierdereuille.android.AndroBib.BibTexParser.BibTexEntry;
import net.barbierdereuille.android.AndroBib.BibTexParser.BibTexItem;
import net.barbierdereuille.android.AndroBib.BibTexParser.BibTexPreamble;
import net.barbierdereuille.android.AndroBib.BibTexParser.BibTexString;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

public class BibTexProvider extends ContentProvider {

	private static final String TAG = "BibTexProvider";

    private static final String DATABASE_NAME = "bibtex.db";
    private static final int DATABASE_VERSION = 3;
    private static final String GLOBAL_TABLE = "global";
    private static final String BIBTEX_TABLE = "bibtex";
    private static final String AUTHORS_TABLE = "authors";
    private static final String FIELDS_TABLE = "fields";
    private static final String STRINGS_TABLE = "strings";
    private static final String PREAMBLES_TABLE = "preambles";

    private static final UriMatcher sUriMatcher;
    private static final HashMap<String,String> sConfigProjectionMap;
    private static final HashMap<String,String> sReferenceProjectionMap;
    private static final HashMap<String,String> sAuthorProjectionMap;
    private static final HashMap<String,String> sFieldProjectionMap;
    private static final HashMap<String,String> sStringProjectionMap;
    private static final HashMap<String,String> sPreambleProjectionMap;
    
    private static final int GLOBAL           = 1;
    
    private static final int REFERENCE        = 11;
    private static final int REFERENCE_ID     = 12;
    private static final int REFERENCE_FILTER = 13;
    
    private static final int AUTHOR           = 21;
    private static final int AUTHOR_ID        = 22;
    private static final int AUTHOR_FILTER    = 23;

    private static final int FIELD            = 31;
    private static final int FIELD_ID         = 32;
    private static final int FIELD_FILTER     = 33;
    private static final int FIELD_REFERENCE  = 34;
    
    private static final int STRING           = 41;
    private static final int STRING_ID        = 42;
    
    private static final int PREAMBLE         = 51;
    private static final int PREAMBLE_ID      = 52;

    /**
     * This class helps open, create, and upgrade the database file.
     */
    private static class DatabaseHelper extends SQLiteOpenHelper {

        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        // Creates all the tables as needed
        @Override
        public void onCreate(SQLiteDatabase db) {
        	db.execSQL("CREATE TABLE " + GLOBAL_TABLE + " ("
        			+ GlobalColumn._ID + " INTEGER PRIMARY KEY,"
        			+ GlobalColumn.FILE + " TEXT,"
        			+ GlobalColumn.TIMESTAMP + " INTEGER"
        			+ ");");
            db.execSQL("CREATE TABLE " + BIBTEX_TABLE + " ("
                    + BibTexColumn._ID + " INTEGER PRIMARY KEY,"
                    + BibTexColumn.KEY + " TEXT,"
                    + BibTexColumn.TYPE + " TEXT"
                    + ");");
            db.execSQL("CREATE TABLE " + AUTHORS_TABLE + " ("
            		+ AuthorColumn._ID + " INTEGER PRIMARY KEY,"
            		+ AuthorColumn.NAME + " TEXT"
            		//+ AuthorColumn.INSTITUTION + " TEXT"
            		+ ");");
            db.execSQL("CREATE TABLE " + FIELDS_TABLE + " ("
            		+ FieldColumn._ID + " INTEGER PRIMARY KEY,"
            		+ FieldColumn.TYPE + " TEXT,"
            		+ FieldColumn.CONTENT + " TEXT,"
            		+ FieldColumn.REFERENCE + " INTEGER"
            		+ ");");
            db.execSQL("CREATE TABLE " + STRINGS_TABLE + " ("
            		+ StringColumn._ID + " INTEGER PRIMARY KEY,"
            		+ StringColumn.NAME + " TEXT,"
            		+ StringColumn.CONTENT + " TEXT"
            		+ ");");
            db.execSQL("CREATE TABLE " + PREAMBLES_TABLE + " ("
            		+ PreambleColumn._ID + " INTEGER PRIMARY KEY,"
            		+ PreambleColumn.CONTENT + " TEXT"
            		+ ");");
            ContentValues values = new ContentValues();
            values.put(GlobalColumn.FILE, "");
            values.put(GlobalColumn.TIMESTAMP, 0);
            db.insert(GLOBAL_TABLE, null, values);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.d(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            dropTables(db);
            onCreate(db);
        }
        
        private void dropTables(SQLiteDatabase db) {
            db.execSQL("DROP TABLE IF EXISTS " + GLOBAL_TABLE);
            db.execSQL("DROP TABLE IF EXISTS " + BIBTEX_TABLE);
            db.execSQL("DROP TABLE IF EXISTS " + AUTHORS_TABLE);
            db.execSQL("DROP TABLE IF EXISTS " + FIELDS_TABLE);        	
            db.execSQL("DROP TABLE IF EXISTS " + STRINGS_TABLE);
            db.execSQL("DROP TABLE IF EXISTS " + PREAMBLES_TABLE);
        }

    }

    private DatabaseHelper mOpenHelper;

	@Override
	public int delete(Uri arg0, String arg1, String[] arg2) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
        case GLOBAL:
        	return GlobalColumn.CONTENT_ITEM_TYPE;
        case REFERENCE:
        case REFERENCE_FILTER:
        	return BibTexColumn.CONTENT_TYPE;
        case REFERENCE_ID:
        	return BibTexColumn.CONTENT_ITEM_TYPE;
        case AUTHOR:
        case AUTHOR_FILTER:
        	return AuthorColumn.CONTENT_TYPE;
        case AUTHOR_ID:
        	return AuthorColumn.CONTENT_ITEM_TYPE;
        case FIELD:
        case FIELD_FILTER:
        case FIELD_REFERENCE:
        	return FieldColumn.CONTENT_TYPE;
        case FIELD_ID:
        	return FieldColumn.CONTENT_ITEM_TYPE;
        default:
        	throw new IllegalArgumentException("Unknown URI " + uri);
        }
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
        switch (sUriMatcher.match(uri)) {
        }
		return null;
	}

	@Override
	public boolean onCreate() {
		mOpenHelper = new DatabaseHelper(getContext());
		Log.d(TAG, "Creating BibTexProider");
		//initDbFromFile(null);
		return true;
	}
	
	private void initDbFromFile(String filename) {
		SQLiteDatabase db = mOpenHelper.getWritableDatabase();
		SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
		qb.setTables(GLOBAL_TABLE);
		qb.setProjectionMap(sConfigProjectionMap);
		String[] projection = { GlobalColumn.FILE, GlobalColumn.TIMESTAMP };
		Cursor c = qb.query(db, projection, null, null, null, null, null);
		c.moveToFirst();
		String dbFile = c.getString(0);
		Long timestamp = c.getLong(1);
		c.close();
		String file = (filename == null) ? dbFile : filename;
		if(file.length() == 0) {
			Log.d(TAG, "No filename in database.");
			return;
		}
		File f = new File(file);
		if((filename != null && filename.equals(dbFile) && f.lastModified() <= timestamp) ||
				!f.exists()) {
			Log.d(TAG, "Database is already up-to-date or the file selected doesn't exist.");
			return;
		}
		Log.d(TAG, "Filename is " + filename + ". f.lastModified() = " + f.lastModified() + " timestamp = " + timestamp);
		try {
			// Now, parse the file
			clearEntries(db);
			parseFile(f, db);
			ContentValues configValues = new ContentValues();
			configValues.put(GlobalColumn.FILE, file);
			timestamp = Long.valueOf(f.lastModified());
			configValues.put(GlobalColumn.TIMESTAMP, timestamp);
			db.update(GLOBAL_TABLE, configValues, null, null);
		} catch(FileNotFoundException ex) {
			resetConfig(db);
			clearReferences(db);
		} catch(IOException ex) {
			resetConfig(db);
			clearReferences(db);
		}
	}
	
	private void clearEntries(SQLiteDatabase db) {
		db.delete(BIBTEX_TABLE, null, null);
		db.delete(FIELDS_TABLE, null, null);
		db.delete(STRINGS_TABLE, null, null);
		db.delete(PREAMBLES_TABLE, null, null);
	}
	
	private void parseFile(File f, SQLiteDatabase db) throws FileNotFoundException, IOException {
		BibTexParser parser = new BibTexParser(f);
		List<BibTexItem> list = parser.parse();
		Iterator<BibTexItem> it = list.iterator();
		while(it.hasNext()) {
			BibTexItem item = it.next();
			switch(item.type()) {
			case ENTRY: {
				BibTexEntry entry = (BibTexEntry) item;
				long id = insertEntry(entry.type, entry.key, db);
				Log.d(TAG, "Added entry @" + entry.type + ": " + entry.key);
				if(id != -1) {
					Iterator<Pair<String,String> > it_f = entry.fields.iterator();
					while(it_f.hasNext()) {
						Pair<String,String> e = it_f.next();
						insertField(id, e.first, e.second, db);
					}
					String[] cols = {"_ID"};
					Cursor c = db.query(FIELDS_TABLE, cols, FieldColumn.REFERENCE+"="+id, null, null, null, null);
					if(c.getCount() != entry.fields.size()) {
						throw new SQLException("Failed to insert " + (entry.fields.size() - c.getCount()) + " fields");
					}
					c.close();
				}
				break;
			}
			case STRING: {
				BibTexString str = (BibTexString) item;
				insertString(str.name, str.content, db);
				Log.d(TAG, "Added string " + str.name);
				break;
			}
			case PREAMBLE: {
				BibTexPreamble pre = (BibTexPreamble) item;
				insertPreamble(pre.content, db);
				Log.d(TAG, "Added preamble");
				break;
			}
			}
		}
	}
	
	private long insertEntry(String type, String key, SQLiteDatabase db) {
		if(db == null) {
			db = mOpenHelper.getWritableDatabase();
		}
		ContentValues values = new ContentValues();
		values.put(BibTexColumn.TYPE, type);
		values.put(BibTexColumn.KEY, key);
		long rowId = db.insert(BIBTEX_TABLE, null, values);
        if (rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(BibTexColumn.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(noteUri, null);
        }
        return rowId;
	}

	private long insertField(long refId, String type, String content, SQLiteDatabase db) {
		if(db == null) {
			db = mOpenHelper.getWritableDatabase();
		}
		ContentValues values = new ContentValues();
		values.put(FieldColumn.TYPE, type);
		values.put(FieldColumn.REFERENCE, refId);
		values.put(FieldColumn.CONTENT, content);
		long rowId = db.insert(FIELDS_TABLE, null, values);
        if (rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(FieldColumn.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(noteUri, null);
        }
        return rowId;
	}

	private void clearReferences(SQLiteDatabase db) {
		db.execSQL("DELETE FROM " + BIBTEX_TABLE);
		db.execSQL("DELETE FROM " + AUTHORS_TABLE);
		db.execSQL("DELETE FROM " + FIELDS_TABLE);
	}

	private long insertString(String name, String content, SQLiteDatabase db) {
		if(db == null) {
			db = mOpenHelper.getWritableDatabase();
		}
		ContentValues values = new ContentValues();
		values.put(StringColumn.NAME, name);
		values.put(StringColumn.CONTENT, content);
		long rowId = db.insert(STRINGS_TABLE, null, values);
		if(rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(StringColumn.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(noteUri, null);
		}
		return rowId;
	}

	private long insertPreamble(String content, SQLiteDatabase db) {
		if(db == null) {
			db = mOpenHelper.getWritableDatabase();
		}
		ContentValues values = new ContentValues();
		values.put(PreambleColumn.CONTENT, content);
		long rowId = db.insert(PREAMBLES_TABLE, null, values);
		if(rowId > 0) {
            Uri noteUri = ContentUris.withAppendedId(PreambleColumn.CONTENT_URI, rowId);
            getContext().getContentResolver().notifyChange(noteUri, null);
		}
		return rowId;
	}

	private void resetConfig(SQLiteDatabase db) {
        ContentValues values = new ContentValues();
        values.put(GlobalColumn.FILE, "");
        values.put(GlobalColumn.TIMESTAMP, 0);
        db.update(GLOBAL_TABLE, values, null, null);
	}
	
	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String orderBy = sortOrder;
        switch(sUriMatcher.match(uri))
        {
        case GLOBAL:
        	qb.setTables(GLOBAL_TABLE);
        	qb.setProjectionMap(sConfigProjectionMap);
        	break;
        case REFERENCE_ID:
        	qb.appendWhere(BibTexColumn._ID + "=" + uri.getPathSegments().get(1));
        case REFERENCE:
        	qb.setTables(BIBTEX_TABLE);
        	qb.setProjectionMap(sReferenceProjectionMap);
        	if(TextUtils.isEmpty(sortOrder)) {
        		orderBy = BibTexColumn.DEFAULT_SORT_ORDER;
        	}
        	break;
        case AUTHOR_ID:
        	qb.appendWhere(AuthorColumn._ID + "=" + uri.getPathSegments().get(2));
        case AUTHOR:
        	qb.setTables(AUTHORS_TABLE);
        	qb.setProjectionMap(sAuthorProjectionMap);
        	if(TextUtils.isEmpty(sortOrder)) {
        		orderBy = AuthorColumn.DEFAULT_SORT_ORDER;
        	}
        	break;
        case FIELD_ID:
        	qb.appendWhere(FieldColumn._ID + "=" + uri.getPathSegments().get(1));
        case FIELD:
        	qb.setTables(FIELDS_TABLE);
        	qb.setProjectionMap(sFieldProjectionMap);
        	break;
        case FIELD_REFERENCE:
        	qb.setTables(FIELDS_TABLE);
        	qb.setProjectionMap(sFieldProjectionMap);
        	qb.appendWhere(FieldColumn.REFERENCE + "=" + uri.getPathSegments().get(1));
        	break;
        case STRING_ID:
        	qb.appendWhere(StringColumn._ID + "=" + uri.getPathSegments().get(2));
        case STRING:
        	Log.d(TAG, "Request string");
        	qb.setTables(STRINGS_TABLE);
        	qb.setProjectionMap(sStringProjectionMap);
        	if(TextUtils.isEmpty(sortOrder)) {
        		orderBy = StringColumn.DEFAULT_SORT_ORDER;
        	}
        	break;
        case FIELD_FILTER:
        case AUTHOR_FILTER:
        case REFERENCE_FILTER:
        	throw new InvalidParameterException("This URI is not yet available: " + uri);
        default:
        	throw new IllegalArgumentException("Unknown URI " + uri);
        }
        // Get the database and run the query
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);

        // Tell the cursor what uri to watch, so it knows when its source data changes
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
        //SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = 0;
        switch(sUriMatcher.match(uri))
        {
        case GLOBAL:
        	if(values.containsKey(GlobalColumn.FILE)) {
        		initDbFromFile(values.getAsString(GlobalColumn.FILE));
        		count = 1;
        	}
        	break;
        default:
        	throw new IllegalArgumentException("Cannot update for URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
	}

	static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "global", GLOBAL);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "reference", REFERENCE);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "reference/#", REFERENCE_ID);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "reference/filter/*", REFERENCE_FILTER);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "author", AUTHOR);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "author/#", AUTHOR_ID);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "author/filter/*", AUTHOR_FILTER);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "field", FIELD);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "field/#", FIELD_ID);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "field/reference/#", FIELD_REFERENCE);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "field/filter/*", FIELD_FILTER);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "string", STRING);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "string/#", STRING_ID);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "preamble", PREAMBLE);
        sUriMatcher.addURI(AndroBib.AUTHORITY, "preamble/#", PREAMBLE_ID);
        
        sConfigProjectionMap = new HashMap<String,String>();
        sConfigProjectionMap.put(GlobalColumn._ID, GlobalColumn._ID);
        sConfigProjectionMap.put(GlobalColumn.FILE, GlobalColumn.FILE);
        sConfigProjectionMap.put(GlobalColumn.TIMESTAMP, GlobalColumn.TIMESTAMP);
        
        sReferenceProjectionMap = new HashMap<String,String>();
        sReferenceProjectionMap.put(BibTexColumn._ID, BibTexColumn._ID);
        sReferenceProjectionMap.put(BibTexColumn.TYPE, BibTexColumn.TYPE);
        sReferenceProjectionMap.put(BibTexColumn.KEY, BibTexColumn.KEY);
        
        sAuthorProjectionMap = new HashMap<String,String>();
        sAuthorProjectionMap.put(AuthorColumn._ID, AuthorColumn._ID);
        sAuthorProjectionMap.put(AuthorColumn.NAME, AuthorColumn.NAME);
        // sAuthorProjectionMap.put(AuthorColumn.INSTITUTION, AuthorColumn.INSTITUTION);
        
        sFieldProjectionMap = new HashMap<String,String>();
        sFieldProjectionMap.put(FieldColumn._ID, FieldColumn._ID);
        sFieldProjectionMap.put(FieldColumn.TYPE, FieldColumn.TYPE);
        sFieldProjectionMap.put(FieldColumn.CONTENT, FieldColumn.CONTENT);
        sFieldProjectionMap.put(FieldColumn.REFERENCE, FieldColumn.REFERENCE);
        
        sStringProjectionMap = new HashMap<String,String>();
        sStringProjectionMap.put(StringColumn._ID, StringColumn._ID);
        sStringProjectionMap.put(StringColumn.NAME, StringColumn.NAME);
        sStringProjectionMap.put(StringColumn.CONTENT, StringColumn.CONTENT);
        
        sPreambleProjectionMap = new HashMap<String,String>();
        sPreambleProjectionMap.put(PreambleColumn._ID, PreambleColumn._ID);
        sPreambleProjectionMap.put(PreambleColumn.CONTENT, PreambleColumn.CONTENT);
	}
}
